/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.navigation;

import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.Pair;
import android.support.v4.util.SparseArrayCompat;
import android.util.AttributeSet;

import androidx.navigation.common.R;

import java.util.ArrayDeque;
import java.util.ArrayList;

/**
 * NavDestination represents one node within an overall navigation graph.
 *
 * <p>Each destination is associated with a {@link Navigator} which knows how to navigate to this
 * particular destination.</p>
 *
 * <p>Destinations declare a set of {@link #putAction(int, int) actions} that they
 * support. These actions form a navigation API for the destination; the same actions declared
 * on different destinations that fill similar roles allow application code to navigate based
 * on semantic intent.</p>
 *
 * <p>Each destination has a set of {@link #getDefaultArguments() default arguments} that will
 * be applied when {@link NavController#navigate(int, Bundle) navigating} to that destination.
 * These arguments can be overridden at the time of navigation.</p>
 */
public class NavDestination {

    /**
     * Retrieve a suitable display name for a given id.
     * @param context Context used to resolve a resource's name
     * @param id The id to get a display name for
     * @return The resource's name if it is a valid id or just the id itself if it is not
     * a valid resource
     */
    @NonNull
    static String getDisplayName(@NonNull Context context, int id) {
        try {
            return context.getResources().getResourceName(id);
        } catch (Resources.NotFoundException e) {
            return Integer.toString(id);
        }
    }

    private final Navigator mNavigator;
    private NavGraph mParent;
    private int mId;
    private CharSequence mLabel;
    private Bundle mDefaultArgs;
    private ArrayList<NavDeepLink> mDeepLinks;
    private SparseArrayCompat<NavAction> mActions;

    /**
     * NavDestinations should be created via {@link Navigator#createDestination}.
     */
    public NavDestination(@NonNull Navigator<? extends NavDestination> navigator) {
        mNavigator = navigator;
    }

    /**
     * Called when inflating a destination from a resource.
     *
     * @param context local context performing inflation
     * @param attrs attrs to parse during inflation
     */
    @CallSuper
    public void onInflate(@NonNull Context context, @NonNull AttributeSet attrs) {
        final TypedArray a = context.getResources().obtainAttributes(attrs,
                R.styleable.Navigator);
        setId(a.getResourceId(R.styleable.Navigator_android_id, 0));
        setLabel(a.getText(R.styleable.Navigator_android_label));
        a.recycle();
    }

    void setParent(NavGraph parent) {
        mParent = parent;
    }

    /**
     * Gets the {@link NavGraph} that contains this destination. This will be set when a
     * destination is added to a NavGraph via {@link NavGraph#addDestination}.
     * @return
     */
    @Nullable
    public NavGraph getParent() {
        return mParent;
    }

    /**
     * Returns the destination's unique ID. This should be an ID resource generated by
     * the Android resource system.
     *
     * @return this destination's ID
     */
    @IdRes
    public int getId() {
        return mId;
    }

    /**
     * Sets the destination's unique ID. This should be an ID resource generated by
     * the Android resource system.
     *
     * @param id this destination's new ID
     */
    public void setId(@IdRes int id) {
        mId = id;
    }

    /**
     * Sets the descriptive label of this destination.
     *
     * @param label A descriptive label of this destination.
     */
    public void setLabel(@Nullable CharSequence label) {
        mLabel = label;
    }

    /**
     * Gets the descriptive label of this destination.
     */
    @Nullable
    public CharSequence getLabel() {
        return mLabel;
    }

    /**
     * Returns the destination's {@link Navigator}.
     *
     * @return this destination's navigator
     */
    @NonNull
    public Navigator getNavigator() {
        return mNavigator;
    }

    /**
     * Returns the destination's default arguments bundle.
     *
     * @return the default arguments bundle
     */
    public @NonNull Bundle getDefaultArguments() {
        if (mDefaultArgs == null) {
            mDefaultArgs = new Bundle();
        }
        return mDefaultArgs;
    }

    /**
     * Sets the destination's default arguments bundle.
     *
     * @param args the new bundle to set
     */
    public void setDefaultArguments(@Nullable Bundle args) {
        mDefaultArgs = args;
    }

    /**
     * Merges a bundle of arguments into the current default arguments for this destination.
     * New values with the same keys will replace old values with those keys.
     *
     * @param args arguments to add
     */
    public void addDefaultArguments(@NonNull Bundle args) {
        getDefaultArguments().putAll(args);
    }

    /**
     * Add a deep link to this destination. Matching Uris sent to
     * {@link NavController#onHandleDeepLink(Intent)} will trigger navigating to this destination.
     * <p>
     * In addition to a direct Uri match, the following features are supported:
     * <ul>
     *     <li>Uris without a scheme are assumed as http and https. For example,
     *     <code>www.example.com</code> will match <code>http://www.example.com</code> and
     *     <code>https://www.example.com</code>.</li>
     *     <li>Placeholders in the form of <code>{placeholder_name}</code> matches 1 or more
     *     characters. The String value of the placeholder will be available in the arguments
     *     {@link Bundle} with a key of the same name. For example,
     *     <code>http://www.example.com/users/{id}</code> will match
     *     <code>http://www.example.com/users/4</code>.</li>
     *     <li>The <code>.*</code> wildcard can be used to match 0 or more characters.</li>
     * </ul>
     * These Uris can be declared in your navigation XML files by adding one or more
     * <code>&lt;deepLink app:uri="uriPattern" /&gt;</code> elements as
     * a child to your destination.
     * <p>
     * Deep links added in navigation XML files will automatically replace instances of
     * <code>${applicationId}</code> with the applicationId of your app.
     * Programmatically added deep links should use {@link Context#getPackageName()} directly
     * when constructing the uriPattern.
     * @param uriPattern The uri pattern to add as a deep link
     * @see NavController#onHandleDeepLink(Intent)
     */
    public void addDeepLink(@NonNull String uriPattern) {
        if (mDeepLinks == null) {
            mDeepLinks = new ArrayList<>();
        }
        mDeepLinks.add(new NavDeepLink(uriPattern));
    }

    /**
     * Determines if this NavDestination has a deep link matching the given Uri.
     * @param uri The Uri to match against all deep links added in {@link #addDeepLink(String)}
     * @return The matching {@link NavDestination} and the appropriate {@link Bundle} of arguments
     * extracted from the Uri, or null if no match was found.
     */
    @Nullable
    Pair<NavDestination, Bundle> matchDeepLink(@NonNull Uri uri) {
        if (mDeepLinks == null) {
            return null;
        }
        for (NavDeepLink deepLink : mDeepLinks) {
            Bundle matchingArguments = deepLink.getMatchingArguments(uri);
            if (matchingArguments != null) {
                return Pair.create(this, matchingArguments);
            }
        }
        return null;
    }

    /**
     * Build an array containing the hierarchy from the root down to this destination.
     *
     * @return An array containing all of the ids from the root to this destination
     */
    @NonNull
    int[] buildDeepLinkIds() {
        ArrayDeque<NavDestination> hierarchy = new ArrayDeque<>();
        NavDestination current = this;
        do {
            NavGraph parent = current.getParent();
            if (parent == null || parent.getStartDestination() != current.getId()) {
                hierarchy.addFirst(current);
            }
            current = parent;
        } while (current != null);
        int[] deepLinkIds = new int[hierarchy.size()];
        int index = 0;
        for (NavDestination destination : hierarchy) {
            deepLinkIds[index++] = destination.getId();
        }
        return deepLinkIds;
    }

    /**
     * Returns the destination ID for a given action. This will recursively check the
     * {@link #getParent() parent} of this destination if the action destination is not found in
     * this destination.
     *
     * @param id action ID to fetch
     * @return destination ID mapped to the given action id, or 0 if none
     */
    @Nullable
    public NavAction getAction(@IdRes int id) {
        NavAction destination = mActions == null ? null : mActions.get(id);
        // Search the parent for the given action if it is not found in this destination
        return destination != null
                ? destination
                : getParent() != null ? getParent().getAction(id) : null;
    }

    /**
     * Sets a destination ID for an action ID.
     *
     * @param actionId action ID to bind
     * @param destId destination ID for the given action
     */
    public void putAction(@IdRes int actionId, @IdRes int destId) {
        putAction(actionId, new NavAction(destId));
    }

    /**
     * Sets a destination ID for an action ID.
     *
     * @param actionId action ID to bind
     * @param action action to associate with this action ID
     */
    public void putAction(@IdRes int actionId, @NonNull NavAction action) {
        if (actionId == 0) {
            throw new IllegalArgumentException("Cannot have an action with actionId 0");
        }
        if (mActions == null) {
            mActions = new SparseArrayCompat<>();
        }
        mActions.put(actionId, action);
    }

    /**
     * Unsets the destination ID for an action ID.
     *
     * @param actionId action ID to remove
     */
    public void removeAction(@IdRes int actionId) {
        if (mActions == null) {
            return;
        }
        mActions.delete(actionId);
    }

    /**
     * Navigates to this destination.
     *
     * <p>Uses the {@link #getNavigator() configured navigator} to navigate to this destination.
     * Apps should not call this directly, instead use {@link NavController}'s navigation methods
     * to ensure consistent back stack tracking and behavior.</p>
     *
     * @param args arguments to the new destination
     * @param navOptions options for navigation
     */
    @SuppressWarnings("unchecked")
    public void navigate(@Nullable Bundle args, @Nullable NavOptions navOptions) {
        Bundle defaultArgs = getDefaultArguments();
        Bundle finalArgs = new Bundle();
        finalArgs.putAll(defaultArgs);
        if (args != null) {
            finalArgs.putAll(args);
        }
        mNavigator.navigate(this, finalArgs, navOptions);
    }
}
